package com.project.gobang.game;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.gobang.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

// 因为这个 "匹配器" 是全局唯一的, 因此使用 Spring 容器来进行管理也是合理的
@Component
// 这个类表示 "匹配器", 通过这个类负责完成整个匹配功能
public class Matcher {
    // 创建三个匹配队列
    private Queue<UserInfo> normalQueue = new LinkedList<UserInfo>();
    private Queue<UserInfo> highQueue = new LinkedList<UserInfo>();
    private Queue<UserInfo> veryHighQueue = new LinkedList<UserInfo>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RoomManager roomManager;

    // 操作匹配队列方法: 把玩家放到匹配队列
    public void add(UserInfo user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.add(user);
                // 如果有玩家进入匹配队列, 那么就唤醒下面的 wait
                normalQueue.notify();
            }
            System.out.println("玩家: " + user.getUsername() + "加入到 normalQueue 匹配队列中");
        } else if (user.getScore() >= 2000 && user.getScore() < 4000) {
            synchronized (highQueue) {
                highQueue.add(user);
                highQueue.notify();
            }
            System.out.println("玩家: " + user.getUsername() + "加入到 highQueue 匹配队列中");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.add(user);
                veryHighQueue.notify();
            }
            System.out.println("玩家: " + user.getUsername() + "加入到 veryHighQueue 匹配队列中");
        }
    }

    // 退出匹配队列: 当玩家点击停止匹配时, 把玩家从匹配队列中删除掉
    // 这里并不实现 点击停止匹配 相关操作, 这是其他模块去实现的功能, 其他模块直接调用该方法即可
    public void remove(UserInfo user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            System.out.println("玩家: " + user.getUsername() + "移出 normalQueue 匹配队列中");
        } else if (user.getScore() >= 2000 && user.getScore() < 4000) {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
            System.out.println("玩家: " + user.getUsername() + "移出 highQueue 匹配队列中");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.remove(user);
            }
            System.out.println("玩家: " + user.getUsername() + "移出 veryHighQueue 匹配队列中");
        }
    }

    // 在构造方法中创建三个线程, 分别针对这三个匹配队列不断地扫描
    // 当启用这个类时, 就能够让扫描线程去扫描匹配队列
    public Matcher() {
        // 使用匿名内部类实现 run() 方法
        Thread t1 = new Thread() {
            @Override
            public void run() {
                // 扫描 normalQueue
                // 使用 while(true) 来实现 "不停地" 扫描
                while (true) {
                    // 用一个方法来具体实现扫描逻辑
                    handlerMatch(normalQueue);
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(veryHighQueue);
                }
            }
        };
        t3.start();
    }

    // 处理匹配具体的扫描逻辑, 放在这个方法中
    private void handlerMatch(Queue<UserInfo> matchQueue) {
        synchronized (matchQueue) {
            // 因为这块代码中: wait() 和 sendMessage() 两个方法会造成不同的异常.
            // 为了方便, 将这一整块代码使用 try-catch 包裹住.
            try {
                // 1.检查队列中的元素是否有2个及以上(只有一个玩家肯定是玩不了的)
                // 线程被唤醒的时机是不确定的, 因此要重新确认条件是否符合, 这里改成 while 循环
                while (matchQueue.size() < 2) {
                    // 使用哪个对象作为锁, 就用哪个对象来进行 wait
                    matchQueue.wait(); // 线程调用 wait() 方法, 就会在这一行代码等待
                }

                // 2.从队列中取出两个玩家
                UserInfo player1 = matchQueue.poll();
                UserInfo player2 = matchQueue.poll();
                System.out.println("匹配出两个玩家: " + player1.getUsername() + " vs " + player2.getUsername());

                // 3. 获取到玩家的 websocket 会话
                // 获取会话的目的是为了给玩家推送消息, 告诉玩家: 你排到对手了
                WebSocketSession session1 = onlineUserManager.getUserOnlineStatusInHallSession(player1.getUserId());
                WebSocketSession session2 = onlineUserManager.getUserOnlineStatusInHallSession(player2.getUserId());

                // 这里再次进行判定
                // 因为在前面当玩家连接异常或断开时, 就将玩家从匹配队列中移除了.
                // 理论上来说, 匹配队列中的玩家一定是处于在线状态的. 但是! 保不齐就会有什么离奇的 bug
                // 因此双重判定是比较保险的
                if (session1 == null) {
                    // 如果 玩家1 不在线, 就应该把 玩家2 重新放回匹配队列中
                    matchQueue.offer(player2);
                    return;
                }
                if (session2 == null) {
                    // 如果 玩家2 不在线, 就应该把 玩家1 重新放回匹配队列中
                    matchQueue.offer(player1);
                    return;
                }

                // 匹配的两个玩家其实是同一个, 或者同一个玩家入队列了两次.
                // 这种情况理论上也不会出现, 因为在前面就进行了处理:
                //    1) 如果玩家下线, 就会把玩家移出匹配队列
                //    2) 禁止玩家多开
                // 但是不排除前面的逻辑出现离奇的 bug
                if (session1 == session2) {
                    // 把该玩家重新放回队列中
                    matchQueue.offer(player1);
                    return;
                }

                // 4. 把这两个玩家放到一个游戏房间中(这里就需要房间管理器)
                //    1) 两个玩家想进行对战, 就需要先有一个房间
                Room room = new Room();
                //    2) 现在房间有了, 玩家有了, 把他们放在房间管理器中
                roomManager.add(room, player1.getUserId(), player2.getUserId());

                // 5. 给玩家响应: 你匹配到对手了
                // 根据前面的前后端约定, 这里需要返回 message 为 "matchSuccess" 这样的响应
                MatchResponse response1 = new MatchResponse(); // 这个是给 玩家1 的响应
                MatchResponse response2 = new MatchResponse(); // 这个是给 玩家2 的响应

                response1.setSuccess(true);
                response1.setMessage("matchSuccess");
                // 这里需要将 Java对象 转换成 JSON 格式的字符串, 并包装成 WebSocketMessage 发送给前端
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                response2.setSuccess(true);
                response2.setMessage("matchSuccess");
                String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
